using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;


namespace Pharmacy.Ultilities
{
    /// <summary>
    /// Provides support methods for reflection operations.
    /// </summary>
    /// <author>ThangTran</author>
    /// <history>
    /// 2011.02.18 - ThangTran: splited from ViewModelHelper.
    /// 2011.02.24 - ThangTran: added methods that support invoke events.
    /// </history>
    public static class ReflectionHelper
    {
        /// <summary>
        /// Invokes an object by given property path and method name.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="rootObject">Starting object that the property path will be navigated in.</param>
        /// <param name="propertyPath">Property path that can be be single member name or multiple member names, seperated by dots.</param>
        /// <param name="methodName">Name of the method to to be invoked.</param>
        /// <param name="args">Method arguments.</param>
        /// <returns></returns>
        public static T InvokeObjectMethod<T>(object rootObject, string propertyPath, string methodName, params object[] args)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(rootObject != null);
            Debug.Assert(!string.IsNullOrEmpty(methodName));
            // --- End parameters checking code -------------------------------

            // check if the property path is specified
            if (!string.IsNullOrEmpty(propertyPath))
            {
                // move root object to that path
                rootObject = GetObjectValue<object>(rootObject, propertyPath);
            }

            return (T)rootObject
                           .GetType()
                           .GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy)
                           .Invoke(rootObject, args);
        }

        /// <summary>
        /// Indicates the given object has property path or not.
        /// </summary>
        /// <param name="rootObject">Starting object that the property path will be navigated in.</param>
        /// <param name="propertyPath">Property path that can be be single member name or multiple member names, seperated by dots.</param>
        /// <returns></returns>
        public static bool HasMember(object rootObject, string propertyPath)
        {
            try
            {
                object newRoot;
                return GetMember(rootObject, propertyPath, out newRoot) != null;
            }
            catch
            {
                return false;
            } 
        }

        /// <summary>
        /// Gets member information of given property path.
        /// </summary>
        /// <param name="rootObject">Starting object that the property path will be navigated in.</param>
        /// <param name="propertyPath">Property path that can be be single member name or multiple member names, seperated by dots.</param>
        /// <param name="newRoot">Ending object that contains the last property in the property path.</param>
        /// <returns></returns>
        public static MemberInfo GetMember(object rootObject, string propertyPath, out object newRoot)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(rootObject != null);
            Debug.Assert(!string.IsNullOrEmpty(propertyPath));
            // --- End parameters checking code -------------------------------

            newRoot = rootObject;

            if (propertyPath.Contains("."))
            {
                // process all sub paths
                foreach (string subPath in propertyPath.Split('.'))
                {
                    // assign to current root and move to next sub path
                    newRoot = GetObjectValue<object>(newRoot, subPath);
                }
            }

            // property path is a property name
            Type currentType = newRoot.GetType();

            // scan deep in inheritance tree because the private members are strictly hidden
            while (true)
            {
                MemberInfo[] propertyInfo = currentType
                    .GetMember(propertyPath, MemberTypes.All,
                               BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
                Debug.Assert(propertyInfo.Length <= 1);

                if (propertyInfo.Length == 0)                    
                {
                    if (currentType.BaseType != typeof(object) && currentType.BaseType != null)
                    {
                        // try moving deeper
                        currentType = currentType.BaseType;
                        continue;
                    }

                    throw new KeyNotFoundException(string.Format("Member {0} not found in {1}", propertyPath, rootObject));
                }

                return propertyInfo[0];
            }
        }

        /// <summary>
        /// Gets object value by given property path.
        /// </summary>        
        /// <param name="rootObject">Starting object that the property path will be navigated in.</param>
        /// <param name="propertyPath">Property path that can be be single member name or multiple member names, seperated by dots.</param>
        /// <param name="value">New value to be set.</param>
        public static void SetObjectValue(object rootObject, string propertyPath, object value)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(rootObject != null);
            Debug.Assert(!string.IsNullOrEmpty(propertyPath));
            // --- End parameters checking code -------------------------------

            // get member info of given property path
            object newRoot;
            MemberInfo memberInfo = GetMember(rootObject, propertyPath, out newRoot);

            // set its value
            if (memberInfo.MemberType == MemberTypes.Field)
            {
                ((FieldInfo)memberInfo).SetValue(rootObject, value);
            }
            else if (memberInfo.MemberType == MemberTypes.Property)
            {
                ((PropertyInfo)memberInfo)
                    .GetSetMethod()
                    .Invoke(rootObject, new object[] { value });
            }
            else
            {
                // else: not supported
                Debug.Fail("Type not supported: " + memberInfo.MemberType);
            }
        }

        /// <summary>
        /// Gets object value by given property path.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="rootObject">Starting object that the property path will be navigated in.</param>
        /// <param name="propertyPath">Property path that can be be single member name or multiple member names, seperated by dots.</param>
        /// <returns></returns>
        public static T GetObjectValue<T>(object rootObject, string propertyPath)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(rootObject != null);
            Debug.Assert(!string.IsNullOrEmpty(propertyPath));
            // --- End parameters checking code -------------------------------

            // get member info of given property path
            object newRoot;
            MemberInfo memberInfo = GetMember(rootObject, propertyPath, out newRoot);

            // ReSharper disable RedundantIfElseBlock
            if (memberInfo.MemberType == MemberTypes.Field)
            {
                return (T)((FieldInfo)memberInfo).GetValue(rootObject);
            }

            else if (memberInfo.MemberType == MemberTypes.Property)
            {
                return (T)((PropertyInfo)memberInfo)
                               .GetGetMethod()
                               .Invoke(rootObject, null);
            }
            else
            {
                // else: not supported
                Debug.Fail("Type not supported: " + memberInfo.MemberType);

                // ReSharper disable HeuristicUnreachableCode
                return default(T);
                // ReSharper restore HeuristicUnreachableCode
            }
            // ReSharper restore RedundantIfElseBlock
        }

        /// <summary>
        /// Gets method that will be invoked the event is raised.
        /// </summary>
        /// <param name="obj">Object that contains the event.</param>
        /// <param name="eventName">Event Name.</param>
        /// <returns></returns>
        public static MethodInfo GetEventInvoker(object obj, string eventName)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(obj != null);
            Debug.Assert(!string.IsNullOrEmpty(eventName));
            // --- End parameters checking code -------------------------------

            // prepare current processing type
            Type currentType = obj.GetType();

            // try to get special event decleration
            while (true)
            {
                FieldInfo fieldInfo = currentType.GetField(eventName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                if (fieldInfo == null)
                {
                    if (currentType.BaseType != typeof(object) && currentType.BaseType != null)
                    {
                        // move deeper
                        currentType = currentType.BaseType;
                        continue;
                    }

                    Debug.Fail(string.Format("Not found event named {0} in object type {1}", eventName, obj));
                    return null;
                }

                // found
                return ((MulticastDelegate) fieldInfo.GetValue(obj)).Method;
            }
        }

        /// <summary>
        /// Invokes an event in given object with event arguments.
        /// </summary>
        /// <param name="obj">Object that contains the event.</param>
        /// <param name="eventName">Event name.</param>
        /// <param name="eventArgs">Event arguments.</param>
        public static void InvokeEvent(object obj, string eventName, params object[] eventArgs)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(obj != null);
            Debug.Assert(!string.IsNullOrEmpty(eventName));
            // --- End parameters checking code -------------------------------

            GetEventInvoker(obj, eventName).Invoke(obj, eventArgs);
        }
    }
}
